package song;

import options.Options;

import java.util.ArrayList;

/**
 * Created by IntelliJ IDEA.
 * User: Aleksey
 * Date: 02.01.2008
 * Time: 18:53:23
 */
public class Bar {
    private TimeSignature timeSignature;
    private Integer tempo;
    private ArrayList<BarPart> parts;
    private Integer loudness;

    public Bar(Integer tempo, TimeSignature timeSignature) {
        this(tempo, timeSignature, Options.DEFAULT_DOWNBEAT);
    }

    public Bar(Integer tempo, TimeSignature timeSignature, Integer loudness) {
        this.loudness = loudness;
        if (tempo.compareTo(Options.MIN_TEMPO) < 0) {
            tempo = Options.MIN_TEMPO;
        }
        if (tempo.compareTo(Options.MAX_TEMPO) > 0) {
            tempo = Options.MAX_TEMPO;
        }
        this.tempo = tempo;

        if (timeSignature.getBeatUnit().compareTo(Options.MAX_BEAT_UNIT) >= 0 &&
                timeSignature.getBeatNumber().equals(1)) {
            timeSignature.setBeatUnit(Options.MAX_BEAT_UNIT - 1);
        }
        parts = new ArrayList<BarPart>();
        setTimeSignature(timeSignature);
    }

    public Integer getLoudness() {
        return loudness;
    }

    public void setLoudness(Integer loudness) {
        this.loudness = loudness;
    }

    public Integer getFullBarTime() {
        int resolution = Options.RESOLUTION;
        int x = timeSignature.getBeatUnit();
        while (x < 2) {
            resolution *= 2;
            x++;
        }
        while (x > 2) {
            resolution /= 2;
            x--;
        }
        return timeSignature.getBeatNumber() * resolution;
    }

    public Integer getRelativeTimeShift(Integer index) {
        Integer sum = 0;
        for (int i = 0; i < index; i++) {
            int res = Options.RESOLUTION;
            int x = parts.get(i).getLength().getBeatUnit();
            while (x < 2) {
                res *= 2;
                x++;
            }
            while (x > 2) {
                res /= 2;
                x--;
            }
            sum += parts.get(i).getLength().getBeatNumber() * res;
        }
        return sum;
    }

    public Integer getDuration(Integer index) {
        int res = Options.RESOLUTION;
        int x = parts.get(index).getLength().getBeatUnit();
        while (x < 2) {
            res *= 2;
            x++;
        }
        while (x > 2) {
            res /= 2;
            x--;
        }
        return parts.get(index).getLength().getBeatNumber() * res;
    }

    public ArrayList<Integer> getNoteAllocation() {
        ArrayList<Integer> noteAllocation = new ArrayList<Integer>();
        for (int i = 0; i < getPartCount(); i++) {
            if (getPart(i).getNote()) {
                noteAllocation.add(getRelativeTimeShift(i));
            }
        }
        return noteAllocation;
    }

    public void applyNoteAllocation(ArrayList<Integer> noteAllocation) {
        for (int i = 0 ; i < getPartCount(); i++) {
            Boolean note = false;
            for (Integer aNoteAllocation : noteAllocation) {
                if (getRelativeTimeShift(i).equals(aNoteAllocation)) {
                    note = true;
                }
            }
            getPart(i).setNote(note);
        }
    }

    public void clearNotes() {
        for (int i = 0 ; i < getPartCount(); i++) {
            getPart(i).setNote(false);
        }
    }

    public void detalize(Integer level) {
        int lev = level;
        int cnt = 0;
        while (lev % 2 == 0) {
            lev /= 2;
            cnt++;
        }
        if (lev != 1) {
            return;
        }
        int res = Options.RESOLUTION;
        TimeSignature timeSignature = new TimeSignature(true, getTimeSignature().getBeatNumber(), getTimeSignature().getBeatUnit());
        int x = timeSignature.getBeatUnit();
        while (x < 2) {
            res *= 2;
            x++;
        }
        while (x > 2) {
            res /= 2;
            x--;
        }
        int resol = (Options.RESOLUTION * 4 / level);

        if (resol <= res) {
            ArrayList<Integer> noteAllocation = getNoteAllocation();
            parts = new ArrayList<BarPart>();
            int partCount = timeSignature.getBeatNumber() * res / resol;
            for (int i = 0; i < partCount; i++) {
                parts.add(new BarPart(new TimeSignature(true, 1, cnt), false, 0, 100));
            }
            applyNoteAllocation(noteAllocation);
        }
    }

    public void fillEach(Integer level) {
        for (int i = 0; i < getPartCount(); i++) {
            if (i % level == 0) {
                getPart(i).setNote(true);
            }
            else {
                getPart(i).setNote(false);
            }
        }
    }

    public void shiftLeft() {
        for (int i = 0; i < getPartCount(); i++) {
            getPart(i).setNote(i == (getPartCount() - 1) ? false : getPart(i + 1).getNote());
        }
    }

    public void shiftRight() {
        for (int i = getPartCount() - 1; i >= 0; i--) {
            getPart(i).setNote(i == 0 ? false : getPart(i - 1).getNote());
        }
    }

    public Integer getNearestPart(Integer relativeShift) {
        Integer minDist = -1;
        Integer result = 0;
        for (int i = 0; i < getPartCount(); i++) {
            if (minDist < 0 || Math.abs(getRelativeTimeShift(i) - relativeShift) < minDist) {
                minDist = Math.abs(getRelativeTimeShift(i) - relativeShift);
                result = i;
            }
        }
        return result;
    }

    public Integer getTempo() {
        return tempo;
    }

    public void setTempo(Integer tempo) {
        if (tempo.compareTo(Options.MIN_TEMPO) < 0) {
            tempo = Options.MIN_TEMPO;
        }
        if (tempo.compareTo(Options.MAX_TEMPO) > 0) {
            tempo = Options.MAX_TEMPO;
        }
        this.tempo = tempo;
    }

    public void setTimeSignature(TimeSignature timeSignature) {
        if (timeSignature.getBeatUnit().compareTo(Options.MAX_BEAT_UNIT) >= 0 &&
                timeSignature.getBeatNumber().equals(1)) {
            timeSignature.setBeatUnit(Options.MAX_BEAT_UNIT - 1);
        }
        this.timeSignature = timeSignature;
        ArrayList<Integer> noteAllocation = getNoteAllocation();
        parts = new ArrayList<BarPart>();
        parts.add(new BarPart(new TimeSignature(true, timeSignature.getBeatNumber(), timeSignature.getBeatUnit()), false, 0, 0));

        detalize(Options.DEFAULT_DETALIZATION);

        while (getPartCount().compareTo(timeSignature.getBeatNumber()) < 0) {
            ArrayList<BarPart> temp = new ArrayList<BarPart>();
            for (BarPart part : parts) {
                temp.add(part);
            }
            for (BarPart part : temp) {
                dividePart(part);
            }
        }
        applyNoteAllocation(noteAllocation);
    }

    public TimeSignature getTimeSignature() {
        return timeSignature;
    }

    public void uniteParts(Integer index1, Integer index2) {
        ArrayList<BarPart> barParts = new ArrayList<BarPart>();
        for (int i = index1; i <= index2; i++) {
            barParts.add(parts.get(i));
        }
        BarPart newPart = BarPart.uniteParts(barParts);
        for (int i = 0; i < index2 - index1 + 1; i++) {
            parts.remove(index1.intValue());
        }
        parts.add(index1, newPart);
    }

    public void dividePart(BarPart part) {
        Integer index = getBarPartIndex(part);
        if (index != null) {
            dividePart(index);
        }
    }

    public void dividePart(Integer index) {
        if (parts.get(index).getLength().getBeatUnit().equals(Options.MAX_BEAT_UNIT) &&
                parts.get(index).getLength().getBeatNumber().equals(1)) {
            return;
        }
        ArrayList<BarPart> newParts = BarPart.dividePart(parts.get(index));
        parts.remove(index.intValue());
        for (int i = newParts.size() - 1; i >= 0; i--) {
            parts.add(index, newParts.get(i));
        }
    }

    public Integer getPartCount() {
        return parts.size();
    }

    public BarPart getPart(Integer index) {
        return parts.get(index);
    }

    public void initialize(Bar bar) {
        this.timeSignature = bar.timeSignature;
        this.tempo = bar.tempo;
        this.loudness = bar.loudness;

        this.parts.clear();
        for (int i = 0; i < bar.getPartCount(); i++) {
            BarPart newBarPart = new BarPart();
            newBarPart.setLength(bar.getPart(i).getLength());
            newBarPart.setNote(bar.getPart(i).getNote());
            newBarPart.setStyle(bar.getPart(i).getStyle());
            parts.add(newBarPart);
        }
    }

    private Integer getBarPartIndex(BarPart part) {
        for (int i = 0; i < parts.size(); i++) {
            if (parts.get(i).getId().equals(part.getId())) {
                return i;
            }
        }
        return null;
    }
}
